/*
 * Copyright (C) 2009 University of Washington
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package org.opendatakit.collect.android.activities;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import org.json.JSONException;
import org.json.JSONObject;
import org.opendatakit.collect.android.R;
import org.opendatakit.collect.android.application.Collect;
import org.opendatakit.collect.android.listeners.FormLoaderListener;
import org.opendatakit.collect.android.listeners.FormSavedListener;
import org.opendatakit.collect.android.logic.FormController;
import org.opendatakit.collect.android.preferences.PreferencesActivity;
import org.opendatakit.collect.android.provider.FormsProviderAPI.FormsColumns;
import org.opendatakit.collect.android.provider.InstanceProviderAPI;
import org.opendatakit.collect.android.provider.InstanceProviderAPI.InstanceColumns;
import org.opendatakit.collect.android.tasks.FormLoaderTask;
import org.opendatakit.collect.android.tasks.SaveToDiskTask;
import org.opendatakit.collect.android.utilities.FileUtils;
import org.opendatakit.collect.android.views.JQueryJavascriptCallback;
import org.opendatakit.collect.android.views.JQueryODKView;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.RelativeLayout;
import android.widget.Toast;

/**
 * FormEntryActivity is responsible for displaying questions, animating transitions between
 * questions, and allowing the user to enter data.
 * 
 * @author Carl Hartung (carlhartung@gmail.com)
 */
public class FormEntryActivity extends Activity implements FormLoaderListener,
        FormSavedListener {
    private static final String t = "FormEntryActivity";

    // Defines for FormEntryActivity
    private static final boolean EXIT = true;
    private static final boolean DO_NOT_EXIT = false;
    private static final boolean EVALUATE_CONSTRAINTS = true;
    private static final boolean DO_NOT_EVALUATE_CONSTRAINTS = false;

    // Extra returned from gp activity
    public static final String LOCATION_RESULT = "LOCATION_RESULT";

    // Identifies the gp of the form used to launch form entry
    public static final String KEY_FORMPATH = "formpath";
    public static final String KEY_INSTANCEPATH = "instancepath";
    public static final String KEY_INSTANCES = "instances";
    public static final String KEY_SUCCESS = "success";
    public static final String KEY_ERROR = "error";

    // Identifies whether this is a new form, or reloading a form after a screen
    // rotation (or similar)
    private static final String NEWFORM = "newform";

    private static final int MENU_LANGUAGES = Menu.FIRST;
    private static final int MENU_HIERARCHY_VIEW = Menu.FIRST + 1;
    private static final int MENU_SAVE = Menu.FIRST + 2;
    private static final int MENU_PREFERENCES = Menu.FIRST + 3;

    private static final int PROGRESS_DIALOG = 1;
    private static final int SAVING_DIALOG = 2;
	
	private enum FileOps {
		COPY,
		RENAME
	};
	
    // Request codes for returning data from specified intent.
    public static final int IMAGE_CAPTURE_CODE = 1;
    public static final int BARCODE_CAPTURE_CODE = 2;
    public static final int AUDIO_CAPTURE_CODE = 3;
    public static final int VIDEO_CAPTURE_CODE = 4;
    public static final int GEOPOINT_CAPTURE_CODE = 5;
    public static final int IMAGE_CHOOSER_CODE = 7;
    public static final int AUDIO_CHOOSER_CODE = 8;
    public static final int VIDEO_CHOOSER_CODE = 9;
    public static final int GEOPOINT_CHOOSER_CODE = 10;
    
	// valid action strings...
	private static final String BARCODE_CAPTURE = "BARCODE_CAPTURE";
	private static final String IMAGE_CAPTURE = "IMAGE_CAPTURE";
	private static final String IMAGE_CHOOSE = "IMAGE_CHOOSE";
	private static final String IMAGE_VIEW = "IMAGE_VIEW";
	private static final String AUDIO_CAPTURE = "AUDIO_CAPTURE";
	private static final String AUDIO_CHOOSE = "AUDIO_CHOOSE";
	private static final String AUDIO_PLAY = "AUDIO_PLAY";
	private static final String VIDEO_CAPTURE = "VIDEO_CAPTURE";
	private static final String VIDEO_CHOOSE = "VIDEO_CHOOSE";
	private static final String VIDEO_PLAY = "VIDEO_PLAY";
	private static final String GEOPOINT_CAPTURE = "GEOPOINT_CAPTURE";
	private static final String GEOPOINT_CHOOSE = "GEOPOINT_CHOOSE";
	private static final String GEOPOINT_VIEW = "GEOPOINT_VIEW";

    private JQueryODKView mCurrentView = null;

    private AlertDialog mAlertDialog = null;
    private ProgressDialog mProgressDialog = null;
    private String mErrorMessage = null;

    private FormLoaderTask mFormLoaderTask = null;
    private SaveToDiskTask mSaveToDiskTask = null;
    
    private boolean exitAfterSave = false;

    private JQueryJavascriptCallback mJSCallback = null;
    private String formIdWaitingForData = null; 
    private String versionWaitingForData = null; 
    private String instanceIdWaitingForData = null; 
    private  String pathWaitingForData = null; 
    private  String pageWaitingForData = null; 
    private String actionWaitingForData = null; 

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mJSCallback = new JQueryJavascriptCallback(this);
        
        // must be at the beginning of any activity that can be called from an external intent
        try {
            Collect.createODKDirs();
        } catch (RuntimeException e) {
            createErrorDialog(e.getMessage(), EXIT);
            return;
        }

        setContentView(R.layout.form_entry);
        setTitle(getString(R.string.app_name) + " > " + getString(R.string.loading_form));

        mAlertDialog = null;
        mCurrentView = null;

        Boolean newForm = true;
        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey(KEY_FORMPATH)) {
                Collect.mFormPath = savedInstanceState.getString(KEY_FORMPATH);
            }
            if (savedInstanceState.containsKey(NEWFORM)) {
                newForm = savedInstanceState.getBoolean(NEWFORM, true);
            }
            if (savedInstanceState.containsKey(KEY_ERROR)) {
                mErrorMessage = savedInstanceState.getString(KEY_ERROR);
            }
        }

        // If a parse error message is showing then nothing else is loaded
        // Dialogs mid form just disappear on rotation.
        if (mErrorMessage != null) {
            createErrorDialog(mErrorMessage, EXIT);
            return;
        }

        // Check to see if this is a screen flip or a new form load.
        Object data = getLastNonConfigurationInstance();
        if (data instanceof FormLoaderTask) {
            mFormLoaderTask = (FormLoaderTask) data;
        } else if (data instanceof SaveToDiskTask) {
            mSaveToDiskTask = (SaveToDiskTask) data;
        } else if (data == null) {
            if (!newForm) {
                refreshCurrentView();
                return;
            }

            // Not a restart from a screen orientation change (or other).
            Collect.getInstance().setFormController(null);
            Collect.mInstancePath = null;

            Intent intent = getIntent();
            if (intent != null) {
                Uri uri = intent.getData();

                if (getContentResolver().getType(uri) == InstanceColumns.CONTENT_ITEM_TYPE) {
                	// get the formId for this instance...
                	String jrFormId = null;
                	{
                		Cursor instanceCursor = null;
                		try {
                			instanceCursor = getContentResolver().query(uri, null, null, null, null);
		                	if (instanceCursor.getCount() != 1) {
		                        this.createErrorDialog("Bad URI: " + uri, EXIT);
		                        return;
		                    } else {
		                        instanceCursor.moveToFirst();
		                        Collect.mInstancePath =
		                            instanceCursor.getString(instanceCursor
		                                    .getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH));
		                    
		                        jrFormId =
		                            instanceCursor.getString(instanceCursor
		                                    .getColumnIndex(InstanceColumns.JR_FORM_ID));
		                    }
                		} finally {
                			if ( instanceCursor != null ) {
                				instanceCursor.close();
                			}
                		}
                	}
                	
                    String[] selectionArgs = {
                        jrFormId
                    };
                    String selection = FormsColumns.JR_FORM_ID + " like ?";

                    {
                        Cursor formCursor = null;
                        try {
                        	formCursor = getContentResolver().query(FormsColumns.CONTENT_URI, 
                        			null, selection, selectionArgs, null);
	                        if (formCursor.getCount() == 1) {
	                            formCursor.moveToFirst();
	                            Collect.mFormPath =
	                                formCursor.getString(formCursor
	                                        .getColumnIndex(FormsColumns.FORM_FILE_PATH));
	                        } else if (formCursor.getCount() < 1) {
	                            this.createErrorDialog("Parent form does not exist", EXIT);
	                            return;
	                        } else if (formCursor.getCount() > 1) {
	                            this.createErrorDialog("More than one possible parent form", EXIT);
	                            return;
	                        }
                        } finally {
                        	if ( formCursor != null ) {
                        		formCursor.close();
                        	}
                        }
                    }
                } else if (getContentResolver().getType(uri) == FormsColumns.CONTENT_ITEM_TYPE) {
                    Cursor c = null;
                    try {
                    	c = getContentResolver().query(uri, null, null, null, null);
                        if (c.getCount() != 1) {
                            this.createErrorDialog("Bad URI: " + uri, EXIT);
                            return;
                        } else {
                            c.moveToFirst();
                            Collect.mFormPath = c.getString(c.getColumnIndex(FormsColumns.FORM_FILE_PATH));
                        }
                    } finally {
                    	if ( c != null ) {
                    		c.close();
                    	}
                    }
                } else {
                    Log.e(t, "unrecognized URI");
                    this.createErrorDialog("unrecognized URI: " + uri, EXIT);
                    return;
                }

                mFormLoaderTask = new FormLoaderTask();
                mFormLoaderTask.setFormLoaderListener(this);
                mFormLoaderTask.execute(Collect.mFormPath);
                showDialog(PROGRESS_DIALOG);
            }
        }
    }


    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(KEY_FORMPATH, Collect.mFormPath);
        outState.putBoolean(NEWFORM, false);
        outState.putString(KEY_ERROR, mErrorMessage);
    }
    
	public void assertValue( String formId, String version, String instanceId, String path, String value ) {
		Collect.getInstance().getIvProvider().assertValue(formId, version, instanceId, path, value);
	}

	public String getValue( String formId, String version, String instanceId, String path) {
		return Collect.getInstance().getIvProvider().getValue(formId, version, instanceId, path);
	}

	public void saveAllValues( boolean asComplete ) {
		Collect.getInstance().getFormController().saveAllValues(asComplete);
	}
	
	/**
	 * Invoked in response to a button click in the web page.
	 * The action to perform is indicated by the action string.
	 * 
	 * @param tag
	 * @param action
	 */
	public String doAction( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) { // for button or image onclicks...
		
		if ( isWaitingForBinaryData() ) {
			Log.w(t, "Already waiting for data -- ignoring");
			return "IGNORE";
		}
		
		if ( GEOPOINT_CAPTURE.equals(action) ) {
			doGeopointCapture( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( GEOPOINT_CHOOSE.equals(action) ) {
			doGeopointChoose( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( GEOPOINT_VIEW.equals(action) ) {
			doGeopointView( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( BARCODE_CAPTURE.equals(action) ) {
        	doBarcode( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( IMAGE_CAPTURE.equals(action) ) {
	    	doImageCapture( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( IMAGE_CHOOSE.equals(action) ) {
			doImageChoose( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( IMAGE_VIEW.equals(action) ) {
			doImageView( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( AUDIO_CAPTURE.equals(action) ) {
	    	doAudioCapture( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( AUDIO_CHOOSE.equals(action) ) {
	    	doAudioCapture( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( AUDIO_PLAY.equals(action) ) {
	    	doAudioCapture( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( VIDEO_CAPTURE.equals(action) ) {
	    	doVideoCapture( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( VIDEO_CHOOSE.equals(action) ) {
	    	doVideoCapture( formId, version, instanceId, path, page, action, valueMap );
		}
		else if ( VIDEO_PLAY.equals(action) ) {
	    	doVideoCapture( formId, version, instanceId, path, page, action, valueMap );
		} else {
			return "Unknown action type";
		}
		return "OK";
	}

    void doBarcode( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Intent i = new Intent("com.google.zxing.client.android.SCAN");
        try {
        	formIdWaitingForData = formId; 
            versionWaitingForData = version; 
            instanceIdWaitingForData = instanceId; 
            pathWaitingForData = path; 
            pageWaitingForData = page; 
            actionWaitingForData = action; 
            startActivityForResult(i,
            		FormEntryActivity.BARCODE_CAPTURE_CODE);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.barcode_scanner_error), Toast.LENGTH_SHORT)
                    .show();
        }
    }
    
    void doImageCapture( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Intent i = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
        // We give the camera an absolute filename/path where to put the
        // picture because of bug:
        // http://code.google.com/p/android/issues/detail?id=1480
        // The bug appears to be fixed in Android 2.0+, but as of feb 2,
        // 2010, G1 phones only run 1.6. Without specifying the path the
        // images returned by the camera in 1.6 (and earlier) are ~1/4
        // the size. boo.

        // if this gets modified, the onActivityResult in
        // FormEntyActivity will also need to be updated.
        i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT,
            Uri.fromFile(new File(Collect.TMPFILE_PATH)));
        try {
        	formIdWaitingForData = formId; 
            versionWaitingForData = version; 
            instanceIdWaitingForData = instanceId; 
            pathWaitingForData = path;
            pageWaitingForData = page; 
            actionWaitingForData = action; 

            startActivityForResult(i,
            		FormEntryActivity.IMAGE_CAPTURE_CODE);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.activity_not_found, "image capture"),
                Toast.LENGTH_SHORT);
        }
    }
    
    void doImageChoose( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.setType("image/*");

        try {
        	formIdWaitingForData = formId; 
            versionWaitingForData = version; 
            instanceIdWaitingForData = instanceId; 
            pathWaitingForData = path; 
            pageWaitingForData = page; 
            actionWaitingForData = action; 

            startActivityForResult(i, FormEntryActivity.IMAGE_CHOOSER_CODE);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.activity_not_found, "choose image"),
                Toast.LENGTH_SHORT);
        }
	}
    
    void doImageView( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
		String binaryName = mJSCallback.getValue(formId, version, instanceId, path);
        Intent i = new Intent("android.intent.action.VIEW");
        String[] projection = {
            "_id"
        };
        Cursor c = null;
        try {
        	c = getContentResolver().query(
                        android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                        projection, "_data=?",
                        new String[] { binaryName },
                        null);
            if (c.getCount() > 0) {
                c.moveToFirst();
                String id = c.getString(c.getColumnIndex("_id"));

                Log.i(
                    t,
                    "setting view path to: "
                            + Uri.withAppendedPath(
                                android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                                id));

                i.setDataAndType(Uri.withAppendedPath(
                    android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id),
                    "image/*");
                try {
                    startActivity(i);
                } catch (ActivityNotFoundException e) {
                    Toast.makeText(this,
                        getString(R.string.activity_not_found, "view image"),
                        Toast.LENGTH_SHORT);
                }
            }
        } finally {
        	if ( c != null ) {
        		c.close();
        	}
        }
    }
    
    void doAudioCapture( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Intent i = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION);
        i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT,
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString());
        try {
        	formIdWaitingForData = formId; 
            versionWaitingForData = version; 
            instanceIdWaitingForData = instanceId; 
            pathWaitingForData = path; 
            pageWaitingForData = page; 
            actionWaitingForData = action; 
            startActivityForResult(i, FormEntryActivity.AUDIO_CAPTURE_CODE);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.activity_not_found, "audio capture"),
                Toast.LENGTH_SHORT);
        }
    }
    
    void doAudioChoose( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.setType("audio/*");
        try {
        	formIdWaitingForData = formId; 
            versionWaitingForData = version; 
            instanceIdWaitingForData = instanceId; 
            pathWaitingForData = path; 
            pageWaitingForData = page; 
            actionWaitingForData = action; 
            startActivityForResult(i, FormEntryActivity.AUDIO_CHOOSER_CODE);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.activity_not_found, "choose audio"),
                Toast.LENGTH_SHORT);
        }
    }
    
    void doAudioPlay( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
		String binaryName = mJSCallback.getValue(formId, version, instanceId, path);
        Intent i = new Intent("android.intent.action.VIEW");
        File f = new File(binaryName);
        i.setDataAndType(Uri.fromFile(f), "audio/*");
        try {
        	formIdWaitingForData = formId; 
            versionWaitingForData = version; 
            instanceIdWaitingForData = instanceId; 
            pathWaitingForData = path; 
            pageWaitingForData = page; 
            actionWaitingForData = action; 
            startActivity(i);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.activity_not_found, "play audio"),
                Toast.LENGTH_SHORT);
        }
    }
    
    void doVideoCapture( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Intent i = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE);
        i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT,
            Video.Media.EXTERNAL_CONTENT_URI.toString());
        try {
        	formIdWaitingForData = formId; 
            versionWaitingForData = version; 
            instanceIdWaitingForData = instanceId; 
            pathWaitingForData = path; 
            pageWaitingForData = page; 
            actionWaitingForData = action; 

            startActivityForResult(i,
            		FormEntryActivity.VIDEO_CAPTURE_CODE);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.activity_not_found, "capture video"),
                Toast.LENGTH_SHORT);
        }
    }
    
    void doVideoChoose( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.setType("video/*");
        // Intent i =
        // new Intent(Intent.ACTION_PICK,
        // android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
        try {
        	formIdWaitingForData = formId; 
            versionWaitingForData = version; 
            instanceIdWaitingForData = instanceId; 
            pathWaitingForData = path; 
            pageWaitingForData = page; 
            actionWaitingForData = action; 

            startActivityForResult(i,
            		FormEntryActivity.VIDEO_CHOOSER_CODE);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.activity_not_found, "choose video "),
                Toast.LENGTH_SHORT);
        }
    }
    
    void doVideoPlay( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
		String binaryName = mJSCallback.getValue(formId, version, instanceId, path);
        Intent i = new Intent("android.intent.action.VIEW");
        File f = new File(binaryName);
        i.setDataAndType(Uri.fromFile(f), "video/*");
        try {
            startActivity(i);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                getString(R.string.activity_not_found, "video video"),
                Toast.LENGTH_SHORT);
        }
    }

    void doGeopointCapture( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Intent i = null;
        String mapClass = null;
		try {
			if ( valueMap != null ) {
				mapClass = valueMap.getString("intentClass");
			}
		} catch (JSONException e1) {
			e1.printStackTrace();
		}
        
        Class theClass = GeoPointActivity.class;
        if ( mapClass != null ) {
            try {
                // do google maps exist on the device
            	theClass = Class.forName(mapClass);
            	
            } catch (ClassNotFoundException e) {
            	// use the fallback class
            }
        } 
        
    	formIdWaitingForData = formId; 
        versionWaitingForData = version; 
        instanceIdWaitingForData = instanceId; 
        pathWaitingForData = path; 
        pageWaitingForData = page; 
        actionWaitingForData = action; 

        i = new Intent(this, theClass);
        startActivityForResult(i, FormEntryActivity.GEOPOINT_CAPTURE_CODE);
    }

    void doGeopointChoose( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
        Class theClass = GeoPointMapActivity.class;
    	formIdWaitingForData = formId; 
        versionWaitingForData = version; 
        instanceIdWaitingForData = instanceId; 
        pathWaitingForData = path; 
        pageWaitingForData = page; 
        actionWaitingForData = action; 

        Intent i = new Intent(this, theClass);
        startActivityForResult(i, FormEntryActivity.GEOPOINT_CHOOSER_CODE);
    }


    void doGeopointView( String formId, String version, String instanceId, String path, String page, String action, JSONObject valueMap ) {
		String geopointString = mJSCallback.getValue(formId, version, instanceId, path);
		String[] geopointTerms = geopointString.split(" ");
		int len = geopointTerms.length;
		double gp[] = new double[len];
		for ( int i = 0 ; i < len ; ++i ) {
			gp[i] = Double.valueOf(geopointTerms[i]);
		}
        Intent i = new Intent(this, GeoPointMapActivity.class);
        i.putExtra("gp", gp);
        startActivity(i);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
    	Log.i(t, "onActivityResult");
    	try {
    		if ( mCurrentView == null ) return;

			String jsonObject = null;
	        switch (requestCode) {
	        	// NOTE: no handling actions taken for GEOPOINT_VIEW, IMAGE_VIEW, AUDIO_PLAY, VIDEO_PLAY
	        	//
	            case BARCODE_CAPTURE_CODE:
					jsonObject = handleBarcode( formIdWaitingForData, versionWaitingForData, 
							instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, intent );
	                break;
	            case IMAGE_CAPTURE_CODE:
	            {
	                /*
	                 * We saved the image to the tempfile_path, but we really want it to be in:
	                 * /sdcard/odk/instances/[current instnace]/something.jpg so we move it there before
	                 * inserting it into the content provider. Once the android image capture bug gets
	                 * fixed, (read, we move on from Android 1.6) we want to handle images the audio and
	                 * video
	                 */
	                // The intent is empty, but we know we saved the image to the temp file
	
	                File source = new File(Collect.TMPFILE_PATH);
	
	                jsonObject = handleImageCapture( formIdWaitingForData, versionWaitingForData, 
							instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, source, FileOps.RENAME );
	            }
	                break;
	            case IMAGE_CHOOSER_CODE:
	            {
	            	File source = null;
	    	        if (resultCode != Activity.RESULT_CANCELED) {
		                /*
		                 * We have a saved image somewhere, but we really want it to be in:
		                 * /sdcard/odk/instances/[current instnace]/something.jpg so we move it there before
		                 * inserting it into the content provider. Once the android image capture bug gets
		                 * fixed, (read, we move on from Android 1.6) we want to handle images the audio and
		                 * video
		                 */
		
		                // get gp of chosen file
		                Uri selectedImage = intent.getData();
		                String sourceImagePath = getImagePathFromUri((Uri) selectedImage);
		                source = new File(sourceImagePath);
	    	        }
	                
	                jsonObject = handleImageCapture( formIdWaitingForData, versionWaitingForData, 
	    						instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, source, FileOps.COPY );
	            }
	                break;
	            case AUDIO_CAPTURE_CODE:
	            {
					jsonObject = handleAudioCapture( formIdWaitingForData, versionWaitingForData, 
							instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, intent );
	            }
					break;
	            case VIDEO_CAPTURE_CODE:
	            {
					jsonObject = handleVideoCapture( formIdWaitingForData, versionWaitingForData, 
							instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, intent );
	            }
                break;
	            case AUDIO_CHOOSER_CODE:
	            {
					jsonObject = handleAudioCapture( formIdWaitingForData, versionWaitingForData, 
							instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, intent );
	            }
                break;
	            case VIDEO_CHOOSER_CODE:
	            {
					jsonObject = handleVideoCapture( formIdWaitingForData, versionWaitingForData, 
							instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, intent );
	            }
	                break;
	            case GEOPOINT_CAPTURE_CODE:
	            {
					jsonObject = handleGeopoint( formIdWaitingForData, versionWaitingForData, 
							instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, intent );
	            }
	                break;
	            case GEOPOINT_CHOOSER_CODE:
	            {
					jsonObject = handleGeopoint( formIdWaitingForData, versionWaitingForData, 
							instanceIdWaitingForData, pathWaitingForData, actionWaitingForData, resultCode, intent );
	            }
	                break;
	        }
			
			if ( jsonObject != null ) {
				mCurrentView.loadJavascriptUrl("javascript:opendatakitCallback('"+pageWaitingForData+"','"+pathWaitingForData+"','"+actionWaitingForData+"', '{ \"status\":\"OK\", \"result\":\"" + jsonObject + "\"}' )");
			} else {
				mCurrentView.loadJavascriptUrl("javascript:opendatakitCallback('"+pageWaitingForData+"','"+pathWaitingForData+"','"+actionWaitingForData+"', '{ \"status\":\"OK\" }' )");
			}
		} catch ( Exception e ) {
			mCurrentView.loadJavascriptUrl("javascript:opendatakitCallback('"+pageWaitingForData+"','"+pathWaitingForData+"','"+actionWaitingForData+"', '{ \"status\":\"ERROR\", \"result\":\"" + e.toString() + "\"}' )");
    	} finally {
        	formIdWaitingForData = null; 
            versionWaitingForData = null; 
            instanceIdWaitingForData = null; 
            pathWaitingForData = null; 
            pageWaitingForData = null; 
            actionWaitingForData = null; 
        	refreshCurrentView();
    	}
    }

	private String handleGeopoint(String formId, String version, String instanceId, String path, String action, int resultCode, Intent intent) {
		if ( resultCode == Activity.RESULT_CANCELED ) {
			return "CANCELED";
		}
		String s = intent.getStringExtra(FormEntryActivity.LOCATION_RESULT);
		String[] sa = s.split(" ");
        double[] lat_long_alt_acc = new double[sa.length];
        int i = 0;
        for ( String sv : sa ) {
        	lat_long_alt_acc[i++] = Double.parseDouble(sv);
        }
        
        mJSCallback.assertValue( formId, version, instanceId, path, s);
        return null;
	}
	
	private String handleBarcode(String formId, String version, String instanceId, String path, String action, int resultCode, Intent intent) {
		if ( resultCode == Activity.RESULT_CANCELED ) {
			return "CANCELED";
		}
        String answer = intent.getStringExtra("SCAN_RESULT");
        mJSCallback.assertValue( formId, version, instanceId, path, answer);
        return null;
	}


    private String getPathFromUri(Uri uri, String pathKey) {
        if (uri.toString().startsWith("file")) {
            return uri.toString().substring(6);
        } else {
            String[] projection = {
            		pathKey
                };
            Cursor c = null;
            try {
            	c = getContentResolver().query(uri, projection, null, null, null);
            	int column_index = c.getColumnIndexOrThrow(pathKey);
            	String path = null;
	            if (c.getCount() > 0) {
	                c.moveToFirst();
	                path = c.getString(column_index);
	            }
	            return path;
            } finally {
            	if ( c != null ) {
            		c.close();
            	}
            }
        }
    }

    private String getImagePathFromUri(Uri uri) {
    	return getPathFromUri(uri, Images.Media.DATA);
    }

    private String getAudioPathFromUri(Uri uri) {
    	return getPathFromUri(uri, Audio.Media.DATA);
    }

    private String getVideoPathFromUri(Uri uri) {
    	return getPathFromUri(uri, Video.Media.DATA);
    }

    public static String getInstanceFolder() {
        return Collect.mInstancePath.substring(0,
        		Collect.mInstancePath.lastIndexOf(File.separator) + 1);
    }
    
    private void deleteImageMedia(String formId, String version, String instanceId, String path, String binaryName) {
        // get the file path and delete the file

        // There's only 1 in this case, but android 1.6 doesn't implement delete on
        // android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI only on
        // android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI + a #
        String[] projection = {
            Images.ImageColumns._ID
        };
        int del = 0;
        Cursor c = null;
        try {
        	c = getContentResolver().query(
                android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection,
                "_data=?", new String[] { binaryName }, null);
	        if (c.getCount() > 0) {
	            c.moveToFirst();
	            String id = c.getString(c.getColumnIndex(Images.ImageColumns._ID));
	
	            Log.i(
	                t,
	                "attempting to delete: "
	                        + Uri.withAppendedPath(
	                            android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id));
	            del =
	                getContentResolver().delete(
	                    Uri.withAppendedPath(
	                        android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), null,
	                    null);
	        }
        } finally {
        	if ( c != null ) {
                c.close();
        	}
        }

        // clean up variables
        mJSCallback.assertValue( formId, version, instanceId, path, null);
        Log.i(t, "Deleted " + del + " rows from media content provider");
    }

    private void deleteAudioMedia(String formId, String version, String instanceId, String path, String binaryName) {
        // get the file path and delete the file
        File f = new File(binaryName);
        if (!f.delete()) {
            Log.i(t, "Failed to delete " + f);
        }

        // clean up variables
        mJSCallback.assertValue( formId, version, instanceId, path, null);
    }

    private void deleteVideoMedia(String formId, String version, String instanceId, String path, String binaryName) {
        // get the file path and delete the file
        File f = new File(binaryName);
        if (!f.delete()) {
            Log.e(t, "Failed to delete " + f);
        }

        // clean up variables
        mJSCallback.assertValue( formId, version, instanceId, path, null);
    }

	private String handleImageCapture(String formId, String version, String instanceId, String path, String action, int resultCode, File sourceImage, FileOps op) {

		if (resultCode == Activity.RESULT_CANCELED) {
            // request was canceled, so do nothing
            return "CANCELED";
        }
		
        String mInstanceFolder = getInstanceFolder();
        String destImagePath = mInstanceFolder + File.separator + System.currentTimeMillis() + ".jpg";

        File newImage = new File(destImagePath);
        if ( op == FileOps.RENAME ) {
	        if (!sourceImage.renameTo(newImage)) {
	            Log.e(t, "Failed to rename " + sourceImage.getAbsolutePath());
	        } else {
	            Log.i(t, "renamed " + sourceImage.getAbsolutePath() + " to " + newImage.getAbsolutePath());
	        }
        } else {
        	FileUtils.copyFile(sourceImage, newImage);
            Log.i(t, "copied " + sourceImage.getAbsolutePath() + " to " + newImage.getAbsolutePath());
        }

		Uri imageURI = null;
        if (newImage.exists()) {
            // Add the new image to the Media content provider so that the
            // viewing is fast in Android 2.0+
        	ContentValues values = new ContentValues(6);
            values.put(Images.Media.TITLE, newImage.getName());
            values.put(Images.Media.DISPLAY_NAME, newImage.getName());
            values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
            values.put(Images.Media.MIME_TYPE, "image/jpeg");
            values.put(Images.Media.DATA, newImage.getAbsolutePath());

            imageURI = getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values);
            Log.i(t, "Inserting image returned uri = " + imageURI.toString());

			// if you are replacing an answer. delete the previous image using the
	        // content provider.
			String value = mJSCallback.getValue(formId, version, instanceId, path);
	        if (value != null && value.length() != 0) {
				// TODO: confirm that this doesn't delete the source audio if it was chosen.
	        	deleteImageMedia(formId, version, instanceId, path, value);
	        }
			
	        String binarypath = getImagePathFromUri((Uri) imageURI);
	        File newImageFromCP = new File(binarypath);

	        mJSCallback.assertValue( formId, version, instanceId, path, newImageFromCP.getAbsolutePath());
	        Log.i(t, "Setting current answer to: " + newImageFromCP.getAbsolutePath());
        } else {
            Log.e(t, "NO IMAGE EXISTS at: " + newImage.getAbsolutePath());
            Toast.makeText(this, R.string.media_save_failed, Toast.LENGTH_SHORT);
        }
        return null;
	}
	
	private String handleAudioCapture(String formId, String version, String instanceId, String path, String action, int resultCode, Intent intent) {
		if ( resultCode == Activity.RESULT_CANCELED ) {
			return "CANCELED";
		}
        Uri media = intent.getData();
        // when replacing an answer. remove the current media.
		String value = mJSCallback.getValue(formId, version, instanceId, path);
        if (value != null && value.length() != 0) {
			// TODO: confirm that this doesn't delete the source audio if it was chosen.
        	deleteAudioMedia(formId, version, instanceId, path, value);
        }

        // get the file path and create a copy in the instance folder
        String binaryPath = getAudioPathFromUri(media);
        String extension = binaryPath.substring(binaryPath.lastIndexOf("."));
        String destAudioPath = getInstanceFolder() + File.separator + System.currentTimeMillis() + extension;

        File source = new File(binaryPath);
        File newAudio = new File(destAudioPath);
        FileUtils.copyFile(source, newAudio);

        if (newAudio.exists()) {
            // Add the copy to the content provier
            ContentValues values = new ContentValues(6);
            values.put(Audio.Media.TITLE, newAudio.getName());
            values.put(Audio.Media.DISPLAY_NAME, newAudio.getName());
            values.put(Audio.Media.DATE_ADDED, System.currentTimeMillis());
            values.put(Audio.Media.DATA, newAudio.getAbsolutePath());

            Uri AudioURI =
                getContentResolver().insert(Audio.Media.EXTERNAL_CONTENT_URI, values);
            Log.i(t, "Inserting AUDIO returned uri = " + AudioURI.toString());

            mJSCallback.assertValue( formId, version, instanceId, path, newAudio.getAbsolutePath());
            Log.i(t, "Setting current answer to: " + newAudio.getAbsolutePath());
        } else {
            Log.e(t, "Inserting Audio file FAILED");
            Toast.makeText(this, R.string.media_save_failed, Toast.LENGTH_SHORT);
        }
        return null;
	}
	
	private String handleVideoCapture(String formId, String version, String instanceId, String path, String action, int resultCode, Intent intent) {
		if ( resultCode == Activity.RESULT_CANCELED ) {
			return "CANCELED";
		}
		Uri media = intent.getData();
        // you are replacing an answer. remove the media.
		String value = mJSCallback.getValue(formId, version, instanceId, path);
        if (value != null && value.length() != 0) {
			// TODO: confirm that this doesn't delete the source video if it was chosen.
            deleteVideoMedia(formId, version, instanceId, path, value);
        }

        // get the file path and create a copy in the instance folder
        String binaryPath = getVideoPathFromUri(media);
        String extension = binaryPath.substring(binaryPath.lastIndexOf("."));
        String destVideoPath = getInstanceFolder() + File.separator + System.currentTimeMillis() + extension;

        File source = new File(binaryPath);
        File newVideo = new File(destVideoPath);
        FileUtils.copyFile(source, newVideo);

        if (newVideo.exists()) {
            // Add the copy to the content provier
            ContentValues values = new ContentValues(6);
            values.put(Video.Media.TITLE, newVideo.getName());
            values.put(Video.Media.DISPLAY_NAME, newVideo.getName());
            values.put(Video.Media.DATE_ADDED, System.currentTimeMillis());
            values.put(Video.Media.DATA, newVideo.getAbsolutePath());

            Uri VideoURI =
                getContentResolver().insert(Video.Media.EXTERNAL_CONTENT_URI, values);
            Log.i(t, "Inserting VIDEO returned uri = " + VideoURI.toString());

            mJSCallback.assertValue( formId, version, instanceId, path, newVideo.getAbsolutePath());
            Log.i(t, "Setting current answer to " + newVideo.getAbsolutePath());
        } else {
            Log.e(t, "Inserting Video file FAILED");
            Toast.makeText(this, R.string.media_save_failed, Toast.LENGTH_SHORT);
        }
        return null;
	}

	public boolean isWaitingForBinaryData() {
		return (pathWaitingForData != null);
	}


    /**
     * Refreshes the current view. the controller and the displayed view can get out of sync due to
     * dialogs and restarts caused by screen orientation changes, so they're resynchronized here.
     */
    public void refreshCurrentView() {
    	refreshCurrentView(false);
    }
    
    public void refreshCurrentView(boolean asHierarchy) {
    	if ( mCurrentView == null ) {
	        RelativeLayout.LayoutParams lp =
	                new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
	
	        RelativeLayout relativeLayout = (RelativeLayout) findViewById(R.id.rl);
	        relativeLayout.removeAllViews();
	        
	        if ( Collect.getInstance().getFormController() != null ) {

	        	setTitle(getString(R.string.app_name) + " > " + Collect.getInstance().getFormController().getFormTitle());
	
	            // should only be a group here if the event_group is a field-list
	            try {
	            	JQueryODKView v = new JQueryODKView(this, mJSCallback, asHierarchy);
	        		Log.i(t, "created view for group");
	    	        relativeLayout.addView(v, lp);
	    	        mCurrentView = v;
	            } catch (RuntimeException e) {
	                createErrorDialog(e.getMessage(), EXIT);
	                e.printStackTrace();
	                // this is badness to avoid a crash.
	                // really a next view should increment the formcontroller, create the view
	                // if the view is null, then keep the current view and pop an error.
	                mCurrentView = null;
	            }
	    	} else {
	    		Log.i(t, "No FormController defined -- no view added!");
	    	}
    	} else {
    		mCurrentView.invalidate();
    	}
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        menu.removeItem(MENU_LANGUAGES);
        menu.removeItem(MENU_HIERARCHY_VIEW);
        menu.removeItem(MENU_SAVE);
        menu.removeItem(MENU_PREFERENCES);

        menu.add(0, MENU_SAVE, 0, R.string.save_all_answers).setIcon(
            android.R.drawable.ic_menu_save);
        menu.add(0, MENU_HIERARCHY_VIEW, 0, getString(R.string.view_hierarchy)).setIcon(
            R.drawable.ic_menu_goto);
        menu.add(0, MENU_LANGUAGES, 0, getString(R.string.change_language))
                .setIcon(R.drawable.ic_menu_start_conversation)
                .setEnabled(
                    (Collect.getInstance().getFormController().getLanguages() == null || 
                     Collect.getInstance().getFormController().getLanguages().length == 1) ? false
                            : true);
        menu.add(0, MENU_PREFERENCES, 0, getString(R.string.general_preferences)).setIcon(
            android.R.drawable.ic_menu_preferences);
        return true;
    }


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MENU_LANGUAGES:
                createLanguageDialog();
                return true;
            case MENU_SAVE:
                // don't exit
            	saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, DO_NOT_EXIT);
                return true;
            case MENU_HIERARCHY_VIEW:
                if ( mCurrentView != null && mCurrentView instanceof JQueryODKView ) {
                	((JQueryODKView) mCurrentView).loadPage("#hierarchy");
                }
                return true;
            case MENU_PREFERENCES:
                Intent pref = new Intent(this, PreferencesActivity.class);
                startActivity(pref);
                return true;
        }
        return super.onOptionsItemSelected(item);
    }


    /**
     * Attempt to save the answer(s) in the current screen to into the data model.
     * Asynchronous completion -- calls back to saveDataToDisk() when complete.
     * 
     * @param evaluateConstraints
     * @param exitAfterSave
     */
    private void saveAnswersForCurrentScreen(boolean evaluateConstraints, boolean exitAfterSave) {
    	if (mCurrentView != null && mCurrentView instanceof JQueryODKView) {
    		this.exitAfterSave = exitAfterSave;
    		((JQueryODKView) mCurrentView).triggerSaveAnswers(evaluateConstraints); 
    	}
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
            menu.add(0, v.getId(), 0, getString(R.string.clear_answer));
        menu.setHeaderTitle(getString(R.string.edit_prompt));
    }


    @Override
    public boolean onContextItemSelected(MenuItem item) {
        /*
         * We don't have the right view here, so we store the View's ID as the item ID and loop
         * through the possible views to find the one the user clicked on.
         */
        return super.onContextItemSelected(item);
    }


    /**
     * If we're loading, then we pass the loading thread to our next instance.
     */
    @Override
    public Object onRetainNonConfigurationInstance() {
        // if a form is loading, pass the loader task
        if (mFormLoaderTask != null && mFormLoaderTask.getStatus() != AsyncTask.Status.FINISHED)
            return mFormLoaderTask;

        // if a form is writing to disk, pass the save to disk task
        if (mSaveToDiskTask != null && mSaveToDiskTask.getStatus() != AsyncTask.Status.FINISHED)
            return mSaveToDiskTask;

        // mFormEntryController is static so we don't need to pass it.
        if ( (mSaveToDiskTask == null || mSaveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) &&
        	 (mCurrentView != null ) ) {
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, DO_NOT_EXIT);
        }
        return null;
    }

    // Hopefully someday we can use managed dialogs when the bugs are fixed
    /*
     * Ideally, we'd like to use Android to manage dialogs with onCreateDialog() and
     * onPrepareDialog(), but dialogs with dynamic content are broken in 1.5 (cupcake). We do use
     * managed dialogs for our static loading ProgressDialog. The main issue we noticed and are
     * waiting to see fixed is: onPrepareDialog() is not called after a screen orientation change.
     * http://code.google.com/p/android/issues/detail?id=1639
     */


    /**
     * Creates and displays dialog with the given errorMsg.
     */
    private void createErrorDialog(String errorMsg, final boolean shouldExit) {
        mErrorMessage = errorMsg;
        mAlertDialog = new AlertDialog.Builder(this).create();
        mAlertDialog.setIcon(android.R.drawable.ic_dialog_info);
        mAlertDialog.setTitle(getString(R.string.error_occured));
        mAlertDialog.setMessage(errorMsg);
        DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                switch (i) {
                    case DialogInterface.BUTTON1:
                        if (shouldExit) {
                            finish();
                        }
                        break;
                }
            }
        };
        mAlertDialog.setCancelable(false);
        mAlertDialog.setButton(getString(R.string.ok), errorListener);
        mAlertDialog.show();
    }


    /**
     * Saves data and writes it to disk. If exit is set, program will exit after save completes.
     * Complete indicates whether the user has marked the isntancs as complete. If updatedSaveName
     * is non-null, the instances content provider is updated with the new name
     */
    public boolean saveDataToDisk(boolean complete, String updatedSaveName) {
    	// figure  out whether we should exit...
        boolean exit = exitAfterSave;
        exitAfterSave = false;

        if (mSaveToDiskTask != null && mSaveToDiskTask.getStatus() != AsyncTask.Status.FINISHED) {
        	// we have a not-yet-complete save already in progress...
        	Toast.makeText(this, R.string.data_saved_error, Toast.LENGTH_SHORT);
        	return false;
        }

        mSaveToDiskTask =
            new SaveToDiskTask(Collect.getInstance().getFormController(), getIntent().getData(), exit, complete, updatedSaveName);
        mSaveToDiskTask.setFormSavedListener(this);
        mSaveToDiskTask.execute();
        showDialog(SAVING_DIALOG);

        return true;
    }

    /**
     * Called by JavaScript if a save_all_changes(asComplete) call (e.g., via triggerSaveAnswers(asComplete))
     * fails.
     */
    public void cancelSaveDataToDisk() {
    	exitAfterSave = false;
    	Toast.makeText(this, R.string.data_saved_error, Toast.LENGTH_SHORT);
    }
    
    /**
     * Create a dialog with options to save and exit, save, or quit without saving
     */
    private void createQuitDialog() {
        String[] items = {
                getString(R.string.keep_changes), getString(R.string.do_not_save)
        };

        mAlertDialog =
            new AlertDialog.Builder(this)
                    .setIcon(android.R.drawable.ic_dialog_info)
                    .setTitle(getString(R.string.quit_application, 
                    		Collect.getInstance().getFormController().getFormTitle()))
                    .setNeutralButton(getString(R.string.do_not_exit),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int id) {

                                dialog.cancel();

                            }
                        }).setItems(items, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            switch (which) {

                                case 0: // save and exit
                                	saveAnswersForCurrentScreen( isInstanceComplete(false), EXIT);
                                    break;

                                case 1: // discard changes and exit

                                    String selection =
                                        InstanceColumns.INSTANCE_FILE_PATH + " like '"
                                                + Collect.mInstancePath + "'";
                                    
                                    boolean erase = false;
                                    {
                                    	Cursor c = null;
                                    	try {
                                    		c = FormEntryActivity.this.getContentResolver().query(
	                                            InstanceColumns.CONTENT_URI, null, selection, null,
	                                            null);
                                    		erase = (c.getCount() < 1);
                                    	} finally {
                                    		if ( c != null ) {
                                    			c.close();
                                    		}
                                    	}
                                    }
                                    // if it's not already saved, erase everything
                                    if (erase) {
                                        int images = 0;
                                        int audio = 0;
                                        int video = 0;
                                        // delete media first
                                        String instanceFolder =
                                            Collect.mInstancePath.substring(0,
                                                Collect.mInstancePath.lastIndexOf("/") + 1);
                                        Log.i(t, "attempting to delete: " + instanceFolder);

                                        String where =
                                            Images.Media.DATA + " like '" + instanceFolder + "%'";

                                        String[] projection = {
                                            Images.ImageColumns._ID
                                        };

                                        // images
                                        Cursor imageCursor = null;
                                        try {
                                        	imageCursor = getContentResolver().query(
                                                        android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                                                        projection, where, null, null);
	                                        if (imageCursor.getCount() > 0) {
	                                            imageCursor.moveToFirst();
	                                            String id =
	                                                imageCursor.getString(imageCursor
	                                                        .getColumnIndex(Images.ImageColumns._ID));
	
	                                            Log.i(
	                                                t,
	                                                "attempting to delete: "
	                                                        + Uri.withAppendedPath(
	                                                            android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
	                                                            id));
	                                            images =
	                                                getContentResolver()
	                                                        .delete(
	                                                            Uri.withAppendedPath(
	                                                                android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
	                                                                id), null, null);
	                                        }
                                        } finally {
                                        	if ( imageCursor != null ) {
                                                imageCursor.close();
                                        	}
                                        }

                                        // audio
                                        Cursor audioCursor = null;
                                        try {
                                        	audioCursor = getContentResolver().query(
                                                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                                                projection, where, null, null);
	                                        if (audioCursor.getCount() > 0) {
	                                            audioCursor.moveToFirst();
	                                            String id =
	                                                audioCursor.getString(imageCursor
	                                                        .getColumnIndex(Images.ImageColumns._ID));
	
	                                            Log.i(
	                                                t,
	                                                "attempting to delete: "
	                                                        + Uri.withAppendedPath(
	                                                            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
	                                                            id));
	                                            audio =
	                                                getContentResolver()
	                                                        .delete(
	                                                            Uri.withAppendedPath(
	                                                                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
	                                                                id), null, null);
	                                        }
                                        } finally {
                                        	if ( audioCursor != null ) {
                                                audioCursor.close();
                                        	}
                                        }

                                        // video
                                        Cursor videoCursor = null;
                                        try {
                                        	videoCursor = getContentResolver().query(
                                                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                                                projection, where, null, null);
	                                        if (videoCursor.getCount() > 0) {
	                                            videoCursor.moveToFirst();
	                                            String id =
	                                                videoCursor.getString(imageCursor
	                                                        .getColumnIndex(Images.ImageColumns._ID));
	
	                                            Log.i(
	                                                t,
	                                                "attempting to delete: "
	                                                        + Uri.withAppendedPath(
	                                                            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
	                                                            id));
	                                            video =
	                                                getContentResolver()
	                                                        .delete(
	                                                            Uri.withAppendedPath(
	                                                                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
	                                                                id), null, null);
	                                        }
                                        } finally {
                                        	if ( videoCursor != null ) {
                                        		videoCursor.close();
                                        	}
                                        }

                                        Log.i(t, "removed from content providers: " + images
                                                + " image files, " + audio + " audio files,"
                                                + " and " + video + " video files.");
                                        File f = new File(instanceFolder);
                                        if (f.exists() && f.isDirectory()) {
                                            for (File del : f.listFiles()) {
                                                Log.i(t, "deleting file: " + del.getAbsolutePath());
                                                del.delete();
                                            }
                                            f.delete();
                                        }
                                    }

                                    finishReturnInstance();
                                    break;

                                case 2:// do nothing
                                    break;
                            }
                        }
                    }).create();
        mAlertDialog.show();
    }


    /**
     * Creates and displays a dialog allowing the user to set the language for the form.
     */
    private void createLanguageDialog() {
        final String[] languages = Collect.getInstance().getFormController().getLanguages();
        int selected = -1;
        if (languages != null) {
            String language = Collect.getInstance().getFormController().getLanguage();
            for (int i = 0; i < languages.length; i++) {
                if (language.equals(languages[i])) {
                    selected = i;
                }
            }
        }
        mAlertDialog =
            new AlertDialog.Builder(this)
                    .setSingleChoiceItems(languages, selected,
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int whichButton) {
                                // Update the language in the content provider when selecting a new
                                // language
                                ContentValues values = new ContentValues();
                                values.put(FormsColumns.LANGUAGE, languages[whichButton]);
                                String selection = FormsColumns.FORM_FILE_PATH + "=?";
                                String selectArgs[] = {
                                    Collect.mFormPath
                                };
                                int updated =
                                    getContentResolver().update(FormsColumns.CONTENT_URI, values,
                                        selection, selectArgs);
                                Log.i(t, "Updated language to: " + languages[whichButton] + " in "
                                        + updated + " rows");

                                Collect.getInstance().getFormController().setLanguage(languages[whichButton]);
                                dialog.dismiss();
                                // TODO: verify: no need to save any answers -- everything is already saved...
                                if ( (mSaveToDiskTask == null || mSaveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) &&
                                   	 (mCurrentView != null && mCurrentView instanceof JQueryODKView) ) {	
                                   	saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, DO_NOT_EXIT);
                                }
                                refreshCurrentView();
                            }
                        })
                    .setTitle(getString(R.string.change_language))
                    .setNegativeButton(getString(R.string.do_not_change),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int whichButton) {
                            }
                        }).create();
        mAlertDialog.show();
    }


    /**
     * We use Android's dialog management for loading/saving progress dialogs
     */
    @Override
    protected Dialog onCreateDialog(int id) {
        switch (id) {
            case PROGRESS_DIALOG:
                mProgressDialog = new ProgressDialog(this);
                DialogInterface.OnClickListener loadingButtonListener =
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                            mFormLoaderTask.setFormLoaderListener(null);
                            mFormLoaderTask.cancel(true);
                            mFormLoaderTask = null;
                            finish();
                        }
                    };
                mProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
                mProgressDialog.setTitle(getString(R.string.loading_form));
                mProgressDialog.setMessage(getString(R.string.please_wait));
                mProgressDialog.setIndeterminate(true);
                mProgressDialog.setCancelable(false);
                mProgressDialog.setButton(getString(R.string.cancel_loading_form),
                    loadingButtonListener);
                return mProgressDialog;
            case SAVING_DIALOG:
                mProgressDialog = new ProgressDialog(this);
                DialogInterface.OnClickListener savingButtonListener =
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                            mSaveToDiskTask.setFormSavedListener(null);
                            mSaveToDiskTask.cancel(true);
                        }
                    };
                mProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
                mProgressDialog.setTitle(getString(R.string.saving_form));
                mProgressDialog.setMessage(getString(R.string.please_wait));
                mProgressDialog.setIndeterminate(true);
                mProgressDialog.setCancelable(false);
                mProgressDialog.setButton(getString(R.string.cancel), savingButtonListener);
                mProgressDialog.setButton(getString(R.string.cancel_saving_form),
                    savingButtonListener);
                return mProgressDialog;
        }
        return null;
    }


    /**
     * Dismiss any showing dialogs that we manually manage.
     */
    private void dismissDialogs() {
        if (mAlertDialog != null && mAlertDialog.isShowing()) {
            mAlertDialog.dismiss();
        }
    }


    @Override
    protected void onPause() {
        dismissDialogs();
        // make sure we're not already saving to disk.  if we are, currentPrompt is getting constantly updated
        if ( (mSaveToDiskTask == null || mSaveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) &&
        	 (mCurrentView != null && mCurrentView instanceof JQueryODKView) ) {	
        	saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, DO_NOT_EXIT);
        }
        super.onPause();
    }


    @Override
    protected void onResume() {
        super.onResume();
        
        // TODO: this is all crap
        if (mFormLoaderTask != null) {
    		mFormLoaderTask.setFormLoaderListener(this);
        	if (mFormLoaderTask.getStatus() == AsyncTask.Status.FINISHED) {
                dismissDialog(PROGRESS_DIALOG);
        		if ( mFormLoaderTask.getFormController() == null ) {
        			mFormLoaderTask.setFormLoaderListener(null);
        		    createErrorDialog(mFormLoaderTask.getErrorMsg(), EXIT);
        			mFormLoaderTask = null;
        		} else {
        			mFormLoaderTask.setFormLoaderListener(null);
        			Collect.getInstance().setFormController(mFormLoaderTask.getFormController());
        			mFormLoaderTask = null;
            		refreshCurrentView();
        		}
            }
        }

        if (mSaveToDiskTask != null) {
            mSaveToDiskTask.setFormSavedListener(this);
        }
        if (mErrorMessage != null && (mAlertDialog != null && !mAlertDialog.isShowing())) {
            createErrorDialog(mErrorMessage, EXIT);
            return;
        }
    }


    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_BACK:
                createQuitDialog();
                return true;
        }
        return super.onKeyDown(keyCode, event);
    }


    @Override
    protected void onDestroy() {
        if (mFormLoaderTask != null) {
            mFormLoaderTask.setFormLoaderListener(null);
            // We have to call cancel to terminate the thread, otherwise it
            // lives on and retains the FEC in memory.
            // but only if it's done, otherwise the thread never returns
            if (mFormLoaderTask.getStatus() == AsyncTask.Status.FINISHED) {
                mFormLoaderTask.cancel(true);
                mFormLoaderTask.setFormLoaderListener(null);
                mFormLoaderTask = null;
            }
        }
        if (mSaveToDiskTask != null) {
            mSaveToDiskTask.setFormSavedListener(null);
            // We have to call cancel to terminate the thread, otherwise it
            // lives on and retains the FEC in memory.
            if (mSaveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) {
                mSaveToDiskTask.cancel(false);
            }
        }

        super.onDestroy();

    }

    /**
     * loadingComplete() is called by FormLoaderTask once it has finished loading a form.
     */
    @Override
    public void loadingComplete(FormController fc) {
        dismissDialog(PROGRESS_DIALOG);
        mFormLoaderTask.setFormLoaderListener(null);
        mFormLoaderTask = null;
        
        Collect.getInstance().setFormController(fc);
        
        boolean asHierarchy = false;
        
        // Set saved answer path
        if (Collect.mInstancePath == null) {

            // Create new answer folder.
            String time =
                new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss")
                        .format(Calendar.getInstance().getTime());
            String file =
                Collect.mFormPath.substring(Collect.mFormPath.lastIndexOf('/') + 1, Collect.mFormPath.lastIndexOf('.'));
            String path = Collect.INSTANCES_PATH + File.separator + file + "_" + time;
            if (FileUtils.createFolder(path)) {
                Collect.mInstancePath = path + File.separator + file + "_" + time + ".xml";
            }
        } else {
            // we've just loaded a saved form, so start in the hierarchy view
        	asHierarchy = true;
        }

        // Set the language if one has already been set in the past
        String[] languageTest = fc.getLanguages();
        if (languageTest != null) {
            String defaultLanguage = fc.getLanguage();
            String newLanguage = "";
            String selection = FormsColumns.FORM_FILE_PATH + "=?";
            String selectArgs[] = {
                Collect.mFormPath
            };
            Cursor c = null;
            try {
            	c = getContentResolver().query(FormsColumns.CONTENT_URI, null, selection, selectArgs, null);
                if (c.getCount() == 1) {
                    c.moveToFirst();
                    newLanguage = c.getString(c.getColumnIndex(FormsColumns.LANGUAGE));
                }
            } finally {
            	if ( c != null ) {
            		c.close();
            	}
            }

            // if somehow we end up with a bad language, set it to the default
            try {
            	fc.setLanguage(newLanguage);
            } catch (Exception e) {
            	fc.setLanguage(defaultLanguage);
            }
        }

        refreshCurrentView(asHierarchy);
    }


    /**
     * called by the FormLoaderTask if something goes wrong.
     */
    @Override
    public void loadingError(String errorMsg) {
        dismissDialog(PROGRESS_DIALOG);
        if (errorMsg != null) {
            createErrorDialog(errorMsg, EXIT);
        } else {
            createErrorDialog(getString(R.string.parse_error), EXIT);
        }
    }


    /**
     * Called by SavetoDiskTask if everything saves correctly.
     */
    @Override
    public void savingComplete(int saveStatus) {
        dismissDialog(SAVING_DIALOG);
        switch (saveStatus) {
            case SaveToDiskTask.SAVED:
                Toast.makeText(this, getString(R.string.data_saved_ok), Toast.LENGTH_SHORT).show();
                break;
            case SaveToDiskTask.SAVED_AND_EXIT:
                Toast.makeText(this, getString(R.string.data_saved_ok), Toast.LENGTH_SHORT).show();
                finishReturnInstance();
                break;
            case SaveToDiskTask.SAVE_ERROR:
                Toast.makeText(this, getString(R.string.data_saved_error), Toast.LENGTH_LONG)
                        .show();
                break;
            default:
                refreshCurrentView();
                // an answer constraint was violated.
                break;
        }
    }


    /**
     * Checks the database to determine if the current instance being edited has already been
     * 'marked completed'. A form can be 'unmarked' complete and then resaved.
     * 
     * @return true if form has been marked completed, false otherwise.
     */
    private boolean isInstanceComplete(boolean end) {
        // default to false if we're mid form
        boolean complete = false;

        // if we're at the end of the form, then check the preferences
        if (end) {
            // First get the value from the preferences
            SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(this);
            complete =
                sharedPreferences.getBoolean(PreferencesActivity.KEY_COMPLETED_DEFAULT, true);
        }

        // Then see if we've already marked this form as complete before
        String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?";
        String[] selectionArgs = {
            Collect.mInstancePath
        };
        Cursor c = null;
        try {
        	c = getContentResolver().query(InstanceColumns.CONTENT_URI, 
        			null, selection, selectionArgs, null);
	        if (c != null && c.getCount() > 0) {
	            c.moveToFirst();
	            String status = c.getString(c.getColumnIndex(InstanceColumns.STATUS));
	            if (InstanceProviderAPI.STATUS_COMPLETE.compareTo(status) == 0) {
	                complete = true;
	            }
	        }
        } finally {
        	if ( c != null ) {
        		c.close();
        	}
        }
        return complete;
    }


    /**
     * Returns the instance that was just filled out to the calling activity, if requested.
     */
    private void finishReturnInstance() {
        String action = getIntent().getAction();
        if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_EDIT.equals(action)) {
            // caller is waiting on a picked form
            String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?";
            String[] selectionArgs = {
                Collect.mInstancePath
            };
            Cursor c = null;
            try {
            	c = getContentResolver().query(InstanceColumns.CONTENT_URI, null, selection, selectionArgs, null);
	            if (c.getCount() > 0) {
	                // should only be one...
	                c.moveToFirst();
	                String id = c.getString(c.getColumnIndex(InstanceColumns._ID));
	                Uri instance = Uri.withAppendedPath(InstanceColumns.CONTENT_URI, id);
	                setResult(RESULT_OK, new Intent().setData(instance));
	            }
            } finally {
            	if ( c != null ) {
            		c.close();
            	}
            }
        }
        finish();
    }
}
